#include <asm/system.h>
#include <cnix/string.h>
#include <cnix/errno.h>
#include <cnix/fs.h>
#include <cnix/kernel.h>
#include <cnix/mm.h>

static struct inode inodes[NR_INODE];
static list_t inode_hash[NR_INODE_HASH];

static list_t ifreelist;
int inode_free_num;
static struct wait_queue * iflwaitq = NULL; /* ifreelist wait queue */

#define ihashfn(dev, ino) (&inode_hash[((dev) ^ (ino)) % NR_INODE_HASH])

static struct inode * get_ihash(dev_t, ino_t);
static void put_ihash(struct inode *);
static BOOLEAN iread(struct inode *, dev_t, ino_t);
static BOOLEAN iwrite(struct inode *, dev_t, ino_t);

/*
 * TODO:
 *   setup a timer to flush inodes in some interval, flash all when shutdown
 */
void init_inode(void)
{
	int i;
	struct inode * inoptr;

	list_head_init(&ifreelist);
	inode_free_num = NR_INODE;

	for(i = 0; i < NR_INODE; i++){
		inoptr = &inodes[i];

		// i_wait will be set to NULL here
		memset(inoptr, 0, sizeof(struct inode));

		inoptr->i_dev = NODEV;	// invalid

		list_add_head(&ifreelist, &inoptr->i_freelist);
		list_head_init(&inoptr->i_hashlist);
	}

	for(i = 0; i < NR_INODE_HASH; i++)
		list_head_init(&inode_hash[i]);
}

static struct inode * get_ihash(dev_t dev, ino_t ino)
{
	list_t * hash, * tmp, * pos;
	struct inode * inoptr;

	hash = ihashfn(dev, ino);
	list_foreach(tmp, pos, hash){
#if 1
		if(tmp->next == tmp)
			DIE("BUG: dev %d ino %d, cannot happen\n", dev, ino);
#endif
		inoptr = list_entry(tmp, i_hashlist, struct inode);
		if((inoptr->i_dev == dev) && (inoptr->i_num == ino))
			return inoptr;
	}

	return NULL;
}

static void put_ihash(struct inode * inoptr)
{
	list_t * hash;

	hash = ihashfn(inoptr->i_dev, inoptr->i_num);
	list_add_head(hash, &inoptr->i_hashlist);
}

void ilock(struct inode * inoptr)
{
loop:
	if(inoptr->i_flags & LOCK_I){
		if(inoptr->i_lockowner == current->pid)
			DIE("BUG: cannot happen\n");

		inoptr->i_flags |= WANT_I;
		sleepon(&inoptr->i_wait);
		goto loop;
	}

	inoptr->i_flags |= LOCK_I;
	inoptr->i_lockowner = current->pid;
}

/*
 * return a LOCKED inode
 */
struct inode * iget(dev_t dev, ino_t ino)
{
	struct inode * tmp;
	struct super_block * sb;

	tmp = NULL;

loop:
	if(dev != NODEV)
		tmp = get_ihash(dev, ino);

	if(tmp){
		if(tmp->i_flags & LOCK_I){
			tmp->i_flags |= WANT_I;
			sleepon(&tmp->i_wait);
			goto loop;
		}

		if(tmp->i_flags & MOUNT_I){
			sb = tmp->i_mounts;

			// avoid looping when iget root inode
			if(!REAL_ROOT(sb->s_rooti)){
				dev = sb->s_rooti->i_dev;
				ino = sb->s_rooti->i_num;
				goto loop;
			}
		}

		// if it's in freelist, dequeue it
		if(tmp->i_count == 0){
			list_del1(&tmp->i_freelist);
			inode_free_num--;
		}

		tmp->i_flags |= LOCK_I;
		tmp->i_lockowner = current->pid;
		tmp->i_count++;

		return tmp;
	}

loop1:
	if(list_empty(&ifreelist)){
		sleepon(&iflwaitq);
		goto loop1;
	}

	tmp = list_entry(ifreelist.next, i_freelist, struct inode);
	if(tmp->i_dev != NODEV)
		list_del1(&tmp->i_hashlist);

	list_del1(&tmp->i_freelist);

	if(tmp->i_dirty){
		if(tmp->i_dev != NODEV)
			iwrite(tmp, tmp->i_dev, tmp->i_num);

		goto loop;
	}	

	if(tmp->i_data)
		tmp->i_ops->free(tmp);

	// NODEV for pipe and new inode
	if(dev != NODEV){
		if(!iread(tmp, dev, ino)){
			list_add_tail(&ifreelist, &tmp->i_freelist);
			return NULL;
		}
	}else{
		// xxx: just for the facility of debuging
		tmp->i_ops = &dummy_inode_operation;
	}

	tmp->i_dev = dev;
	tmp->i_num = ino;
	tmp->i_fnum = NOINO;

	tmp->i_count = 1;

	tmp->i_dirty = 0;
	tmp->i_flags = LOCK_I;
	tmp->i_update = 0;
	tmp->i_lockowner = current->pid;

	// the same reason as b_wait in function of buffer.c 
	//tmp->i_wait = NULL;

	// after inode's data is all filled
	if(dev != NODEV)
		put_ihash(tmp);

	inode_free_num--;

	return tmp;
}

extern void socket_close(struct inode * inoptr);

/*
 * if inode is locked, must be locked by who called iput
 */
void iput(struct inode * inoptr)
{
	struct super_block * sb;

	if(!inoptr)
		DIE("BUG: put a NULL inode\n");

	if(!(inoptr->i_flags & LOCK_I)
		|| (inoptr->i_lockowner != current->pid))
		ilock(inoptr);

	--inoptr->i_count;
	if(inoptr->i_count == 1){
		if(is_pipe(inoptr)){
			inoptr->i_buddy = 0;
			wakeup(&inoptr->i_wait);
			select_wakeup(
				&inoptr->i_select, OPTYPE_READ | OPTYPE_READ
				);
		}
	}else if(inoptr->i_count == 0){
		if(inoptr->i_dev != NODEV){
			// this inoptr is invalid, must be dequeued from hash
			if(list_empty(&inoptr->i_hashlist))
				DIE("BUG: inode must be in hash list\n");

			if(inoptr->i_ops->can_delete(inoptr)){
				inoptr->i_ops->truncate(inoptr, 0);

				sb = sread(inoptr->i_dev);
				sb->s_filesystem->ifree(
					sb, inoptr->i_num
					);

				if(!inoptr->i_data)
					DIE("BUG: cannot happen\n");

				inoptr->i_ops->free(inoptr);
				inoptr->i_data = NULL; // xxx, to be sure
				inoptr->i_dev = NODEV;
				inoptr->i_dirty = 0;

				list_del1(&inoptr->i_hashlist);
			}
		}else if(is_pipe(inoptr)){
			if(inoptr->i_buffer){
				free_one_page((unsigned long)inoptr->i_buffer);
				inoptr->i_buffer = NULL;
			}
		}else if(is_socket(inoptr))
			socket_close(inoptr);

		// add inoptr into free list tail
		list_add_tail(&ifreelist, &inoptr->i_freelist);
		inode_free_num++;

		wakeup(&iflwaitq);
	}else if(inoptr->i_count < 0)
		DIE("BUG: i_count is less than 0, dev %d ino %d\n",
			inoptr->i_dev, inoptr->i_num);

	if(inoptr->i_dirty && (inoptr->i_dev != NODEV) && inoptr->i_data)
		iwrite(inoptr, inoptr->i_dev, inoptr->i_num);

	inoptr->i_flags &= ~LOCK_I;
	inoptr->i_lockowner = 0;

	if(inoptr->i_flags & WANT_I){
		inoptr->i_flags &= ~WANT_I;
		wakeup(&inoptr->i_wait);
	}
}

/*
 * inoptr is not locked
 */
static BOOLEAN iread(struct inode * inoptr, dev_t dev, ino_t ino)
{
	struct super_block * sb;
	struct filesystem * fs;

	sb = sread(dev);
	if(!sb)
		return FALSE;

	inoptr->i_sb = sb;

	fs = sb->s_filesystem;
	if(!fs->iread(inoptr, dev, ino))
		return FALSE;
	
	if(!inoptr->i_ops || !inoptr->i_data)
		DIE("BUG: cannot happen(%x:%x)\n", dev, ino);

	inoptr->i_type = ONHD;

	return TRUE;
}

/*
 * inoptr is not locked
 */
static BOOLEAN iwrite(struct inode * inoptr, dev_t dev, ino_t ino)
{
	struct super_block * sb;
	struct filesystem * fs;

	sb = inoptr->i_sb;
	if(!sb)
		DIE("BUG: cannot happen\n");

	if(inoptr->i_update){
		time_t now;
		time_t atime, mtime, ctime;

		atime = 0;
		mtime = 0;
		ctime = 0;

		now = curclock();

		if(inoptr->i_update & ATIME)
			atime = now;

		if(inoptr->i_update & MTIME)
			mtime = now;

		if(inoptr->i_update & CTIME)
			ctime = now;

		inoptr->i_ops->update(inoptr, atime, mtime, ctime);
	}

	fs = sb->s_filesystem;
	if(!fs->iwrite(inoptr, dev, ino))
		return FALSE;
	
	if(!inoptr->i_ops || !inoptr->i_data)
		DIE("BUG: cannot happen\n");

	inoptr->i_dirty = 0;
	inoptr->i_update = 0;

	return TRUE;
}

struct inode * ialloc(dev_t dev, mode_t mode, int * error)
{
	struct inode inode;
	struct inode * inoptr;
	struct super_block * sb;

	sb = sread(dev);

	// must have been checked
	if(sb->s_roflag){
		*error = -EROFS;
		return NULL;
	}

	if(!sb->s_filesystem->ialloc(sb, &inode, mode)){
		printk("no spare inode on dev(%d,%d)", MAJOR(dev), MINOR(dev));
		*error = -ENOSPC;
		return NULL;
	}

	inoptr = iget(NODEV, inode.i_num);
	if(!inoptr){
		sb->s_filesystem->ifree(sb, inode.i_num);
		kfree(inode.i_data);
		*error = -ENOMEM;
		return NULL;
	}

	inoptr->i_dev = inode.i_dev;
	inoptr->i_num = inode.i_num;
	inoptr->i_ops = inode.i_ops;
	inoptr->i_data = inode.i_data;

	inoptr->i_type = ONHD;
	inoptr->i_count = 1; // xxx
	inoptr->i_dev = dev;
	inoptr->i_dirty = 1;
	inoptr->i_sb = sb;
	inoptr->i_update = (ATIME | MTIME | CTIME);

	// put into hash table
	put_ihash(inoptr);

	return inoptr;
}

void ifree(struct super_block * sb, ino_t ino)
{
	sb->s_filesystem->ifree(sb, ino);
}

int get_iiucount(dev_t dev)
{
	int i, count;
	struct inode * inoptr;

	count = 0;
	for(i = 0, inoptr = &inodes[0]; i < NR_INODE; i++, inoptr++)
		if((inoptr->i_count > 0) && (inoptr->i_dev == dev))
			count += inoptr->i_count; // xxx

	return count;
}

void inval_inode(dev_t dev)
{
	list_t * pos, * tmp;
	struct inode * inoptr;

	list_foreach(tmp, pos, &ifreelist)
	{
		inoptr = list_entry(tmp, i_freelist, struct inode);

		if(inoptr->i_dev == dev)
		{
			list_del1(&inoptr->i_hashlist);

			if(inoptr->i_data)
				inoptr->i_ops->free(inoptr);
		}
	}
}
